﻿/*
 * This file is part of MonoStrategy.
 *
 * Copyright (C) 2010-2011 Christoph Husse
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Affero General Public License as
 *  published by the Free Software Foundation, either version 3 of the
 *  License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Affero General Public License for more details.
 *
 *  You should have received a copy of the GNU Affero General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors: 
 *      # Christoph Husse
 * 
 * Also checkout our homepage: http://monostrategy.codeplex.com/
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace MonoStrategy
{
    public delegate void DOnAddMovable<TSender>(TSender inSender, Movable inMovable);
    public delegate void DOnRemoveMovable<TSender>(TSender inSender, Movable inMovable);

    /// <summary>
    /// The movable manager is the internal interface to pathfinding. Every movable is added here and 
    /// the only (intended) way to change its position is by using <see cref="SetPath"/>. The most
    /// important thing everyone should keep in mind about this path engine: It is deterministic in
    /// the strongest way, meaning it is 100% deterministic within a discrete space-time. This is a very
    /// important foundation for later network synchronization and saving games.
    /// </summary>
    internal partial class MovableManager : SynchronizedManager
    {
        internal static readonly double LOG_2_FACTOR = 1.0 / Math.Log(2.0);

        private readonly List<Movable> m_MarkedForRemoval = new List<Movable>();
        private TopologicalList<Movable> m_Movables;
        internal TerrainDefinition Terrain { get {return Map.Terrain;} }
        internal GameMap Map { get; private set; }
        internal long AvgPlanMillis { get { return 0; /* m_AStar.AvgPlanMillis; */ } }
        internal long MaxPlanMillis { get { return 0; /* m_AStar.MaxPlanMillis; */ } }
        /// <summary>
        /// Width and/or Height in TerrainCells. For performance reasons width and height will always
        /// have the same value and must be a power of two. But don't rely on that outside the path engine.
        /// </summary>
        internal Int32 Size { get; private set; }

        /// <summary>
        /// Will be called once for every added movable.
        /// </summary>
        internal event DOnAddMovable<MovableManager> OnAddMovable;
        /// <summary>
        /// Will be called once for every removed movable.
        /// </summary>
        internal event DOnRemoveMovable<MovableManager> OnRemoveMovable;

        /// <summary>
        /// Can a movable of the given type walk on given cell?
        /// </summary>
        /// <param name="inCell">Starting at (0,0) the grid position on <see cref="Terrain"/> in TerrainCells.</param>
        /// <param name="inMovableType">An integer movable type.</param>
        /// <returns></returns>
        internal bool IsWalkable(Point inCell, MovableType inMovableType)
        {
            return (int)Terrain.GetWallAt(inCell.X, inCell.Y) <= (int)inMovableType;
        }

        /// <summary>
        /// Creates a new instance.
        /// </summary>
        /// <param name="inSize">Desired terrain grid width (height) in TerrainCells. Must be a power of two.</param>
        /// <param name="inCurrentCycle">An initial value for <see cref="CurrentCycle"/>.</param>
        /// <param name="inCycleResolution">Desired value for <see cref="CycleResolution"/>.</param>
        internal MovableManager(GameMap inMap, Int64 inCurrentCycle, Int64 inCycleResolution) : base(inCurrentCycle, inCycleResolution)
        {
            Size = inMap.Size;
            Map = inMap;
            m_Movables = new TopologicalList<Movable>(10, Size, Size);  
        }

        /// <summary>
        /// The only place where new paths are acquired. 
        /// </summary>
        private void AcquirePath(Movable inMovable)
        {
            if (!IsAlignedCycle)
                throw new ApplicationException("Path can not be acquired in current cycle.");

            if ((inMovable.Position.XCycles % (int)CyclePoint.CYCLE_MILLIS) != 0)
                throw new ApplicationException("XCycles is not a multiple of node time.");

            if ((inMovable.Position.YCycles % (int)CyclePoint.CYCLE_MILLIS) != 0)
                throw new ApplicationException("YCycles is not a multiple of node time.");

            // acquire new path
            //if (inMovable.HasJob)
            {
                Map.Routing.SetDynamicPath(
                    inMovable,
                    inMovable.PathTarget,
                    inMovable.MovableType);
            }/*
            else
            {
                m_AStar.SetIdlePath(
                    inMovable,
                    0);
            }*/
        }


        internal override void ProcessCycle()
        {
            base.ProcessCycle();

            Map.Routing.AddOneCycle();

            if (!IsAlignedCycle)
                return;

            lock (Movable.GlobalLock)
            {
                m_Movables.ForEach((movable) =>
                {
                    if (movable.Job == null)
                        return true;

                    movable.Job.Update();

                    return true;
                });

                m_Movables.ForEach((movable) =>
                {
                    if (!movable.IsInvalidated && (movable.ReplanTime >= CurrentCycle))
                    {
                        var currentNode = movable.CurrentNode.Value;
                        var next = movable.CurrentNode.Next;

                        if (next != null)
                        {
                            if (next.Value.Time <= CurrentCycle)
                            {
                                CyclePoint newPos = CyclePoint.FromGrid(next.Value.Position.X, next.Value.Position.Y);
                                CyclePoint backupPos = movable.Position;

                                movable.SetPosition_YouMustNotDoThis(newPos); // this is one of the rare exceptions, where we just have to do it...
                                movable.CurrentNode = next;

                                if (newPos.ToPoint() == movable.PathTarget)
                                {
                                    var handler = movable.ResultHandler;

                                    movable.ResultHandler = null;

                                    if (handler != null)
                                        handler(true);
                                }
                            }
                        }
                    }

                    if (movable.IsInvalidated || (movable.ReplanTime <= CurrentCycle))
                    {
                        AcquirePath(movable);
                    }

                    return true;
                });

                foreach (var movable in m_MarkedForRemoval)
                {
                    Map.Routing.ClearPath(movable);

                    m_Movables.Remove(movable);

                    movable.CurrentNode = null;
                    movable.Parent = null;
                }

                m_MarkedForRemoval.Clear();
            }
        }

        /// <summary>
        /// Even though position interpolation does not really belong here, and instead should be
        /// kept in a renderer or whatever, it requires a bunch of internal knowledge about path
        /// planning if you want to implement it efficiently. And thus it is much cleaner to
        /// put this method here instead of exposing the path planning internals to the public!
        /// </summary>
        /// <param name="inMovable"></param>
        /// <param name="refDirection"></param>
        /// <param name="outXYInterpolation"></param>
        /// <param name="outZInterpolation"></param>
        internal void InterpolateMovablePosition(
            Movable inMovable,
            ref Direction refDirection,
            out CyclePoint outXYInterpolation,
            out double outZInterpolation)
        {
            lock (Movable.GlobalLock)
            {
                outXYInterpolation = inMovable.Position;

                if (inMovable.CurrentNode != null)
                {
                    var currentNodeRef = inMovable.CurrentNode;
                    var currentNode = currentNodeRef.Value;

                    if (currentNode.Direction.HasValue)
                    {
                        var nextNode = currentNodeRef.Next.Value;
                        long nodeStartTime = currentNode.Time;
                        long nodeEndTime = nextNode.Time;
                        int nodeDuration = (int)(nodeEndTime - nodeStartTime);
                        int timeOffset = (int)(CurrentCycle - nodeStartTime);
                        float nextZ = Terrain.GetZShiftAt(nextNode.Position.X, nextNode.Position.Y);
                        float currentZ = Terrain.GetZShiftAt(currentNode.Position.X, currentNode.Position.Y);

                        if (timeOffset >= nodeDuration)
                            timeOffset = nodeDuration;


                        // compute animation progress
                        Point dirVec = DirectionUtils.GetDirectionVector(currentNode.Direction.Value);

                        /*
                         * This is a point where I first made a big mistake and tried to set the movable position directly
                         * to the interpolated value. This is also the reason for the "YouMustNotDoThis" suffix.
                         * It will lead to race conditions (caused by multi-threading) which are extremly difficult to track down. 
                         * Also the interpolation has just nothing to do with path planning but is only interesting
                         * for the visualization so it should stay there...
                         */
                        refDirection = currentNode.Direction.Value;
                        outXYInterpolation = CyclePoint.FromGrid(currentNode.Position.X, currentNode.Position.Y)
                            .AddCycleVector(new Point(dirVec.X * timeOffset, dirVec.Y * timeOffset));
                        outZInterpolation = currentZ + timeOffset * (nextZ - currentZ) / nodeDuration;
                    }
                    else
                    {
                        var pos = inMovable.Position;

                        outZInterpolation = Terrain.GetZShiftAt(pos.XGrid, pos.YGrid);
                    }
                }
                else
                {
                    var pos = inMovable.Position;

                    outZInterpolation = Terrain.GetZShiftAt(pos.XGrid, pos.YGrid);
                }
            }
        }

        /// <summary>
        /// Enumerates movables around the given grid position with increasing distance on average (meaning
        /// is is not strongly sorted by distance, for performance reasons).
        /// </summary>
        /// <param name="inAround"></param>
        /// <param name="inHandler"></param>
        /// <returns></returns>
        internal WalkResult EnumMovablesAround(Point inAround, Func<Movable, WalkResult> inHandler)
        {
            return m_Movables.EnumAround(inAround, (movable) => { return inHandler(movable); });
        }

        internal Movable FindFreeMovableAround(Point inAround, MovableType inType)
        {
             Movable result = null;

            if(WalkResult.Success != EnumMovablesAround(inAround, (movable) =>
                    {
                        if((movable.MovableType != inType) || movable.HasJob)
                            return WalkResult.NotFound;

                        result = movable;

                        return WalkResult.Success;
                    }))
                return null;

            return result;
        }

        /// <summary>
        /// It is not checked whether the movable
        /// can be placed/walk where it is. If it can't walk or is to be placed on a non-movable object, it will 
        /// immediately die. Allocated movables have to be released with <see cref="ReleaseMovable"/>.
        /// On success, <see cref="OnAddMovable"/> will be raised.
        /// </summary>
        internal Movable AddMovable(CyclePoint inPosition, MovableType inMovable)
        {
            Movable movable = new Movable(inPosition, false)
            {
                MovableType = inMovable,
            };

            m_Movables.Add(movable);

            movable.Parent = this;

            if (IsAlignedCycle)
            {
                movable.Path.AddLast(new MovablePathNode()
                {
                    Movable = movable,
                    Position = new Point(movable.Position.XGrid, movable.Position.YGrid),
                    Time = CurrentCycle,
                });
            }

            movable.Path.AddLast(new MovablePathNode()
            {
                Movable = movable,
                Position = new Point(movable.Position.XGrid, movable.Position.YGrid),
                Time = NextAlignedCycle,
            });

            movable.CurrentNode = movable.Path.First;

            if (OnAddMovable != null)
                OnAddMovable(this, movable);

            return movable;
        }

        /// <summary>
        /// Marks the given movable for removal. The manager may decide the specific point of removal on
        /// his convenience. In contrast, the <see cref="OnRemoveMovable"/> event is called BEFORE this method
        /// returns!
        /// </summary>
        internal void MarkMovableForRemoval(Movable inMovable)
        {
            if (inMovable == null)
                return;

            if (inMovable.Parent != this)
                throw new InvalidOperationException("Movable is not attached to this path engine instance.");

            inMovable.Dispose();

            m_MarkedForRemoval.Add(inMovable);

            if (OnRemoveMovable != null)
                OnRemoveMovable(this, inMovable);

            // must be executed after! remove event
            inMovable.Job = null;
        }

        /// <summary>
        /// See <see cref="SetPath"/>.
        /// </summary>
        internal void SetPath(Movable inMovable, Point inTarget)
        {
            SetPath(inMovable, inTarget, null);
        }

        /// <summary>
        /// Sets a path with an optional result handler. It is not guaranteed that a path can be
        /// found, neither that it will be reached. What is guaranteed is that the result handler
        /// is called in any case and only with "true" as parameter if the movable has reached its
        /// destination. Further, a movable with a pending result handler is considered to be non-idle.
        /// If you try to set a path for a movable with a pending result handler ("doing a job"), the 
        /// call will immediately throw an exception!
        /// </summary>
        /// <param name="inMovable">Movable to plan a path for.</param>
        /// <param name="inTarget">Destination for the movable.</param>
        /// <param name="inResultHandler">An optional result handler.</param>
        /// <exception cref="InvalidOperationException">The movable is already doing a job. Call <see cref="Stop"/> first.</exception>
        internal void SetPath(Movable inMovable, Point inTarget, Procedure<bool> inResultHandler)
        {
            Movable movable = (Movable)inMovable;

            if ((movable.ResultHandler != null) && !movable.UserControlable)
                throw new InvalidOperationException("The movable is already doing a job.");

            if (movable.PathTarget == inTarget)
            {
                if (inResultHandler != null)
                    inResultHandler(true);

                return;
            }

            movable.ResultHandler = inResultHandler;
#if DEBUG
            movable.PathStackTrace = new System.Diagnostics.StackTrace(true);
#endif

            // update path information
            movable.PathTarget = inTarget;
            movable.IsInvalidated = true;
        }

        /// <summary>
        /// See <see cref="SetPath"/>.
        /// </summary>
        internal void SetPath(IEnumerable<Movable> inMovables, Point inTarget)
        {
            SetPath(inMovables, inTarget, null);
        }

        /// <summary>
        /// See <see cref="SetPath"/>.
        /// </summary>
        internal void SetPath(IEnumerable<Movable> inMovables, Point inTarget, Procedure<bool> inResultHandler)
        {
            foreach (var movable in inMovables)
            {
                SetPath(movable, inTarget, inResultHandler);
            }
        }

        internal int CountIdleSettlers()
        {
            int idleSettlerCount = 0;

            m_Movables.ForEach((movable) =>
            {
                switch (movable.MovableType)
                {
                    case MovableType.Settler:
                        {
                            if (!movable.HasJob)
                                idleSettlerCount++;
                        } break;
                }

                return true;
            });

            return idleSettlerCount;
        }

        internal void CountMovables(int[] inBuffer)
        {
            Array.Clear(inBuffer, 0, inBuffer.Length);

            m_Movables.ForEach((movable) =>
            {
                switch (movable.MovableType)
                {
                    case MovableType.Grader: inBuffer[(int)SettlerStatisticTypes.Grader]++; break;
                    case MovableType.Constructor: inBuffer[(int)SettlerStatisticTypes.Constructor]++; break;
                    case MovableType.Settler: {
                        if (movable.HasJob)
                        {
                            var job = movable.Job;

                            if(job.GetType() == typeof(JobCarrying))
                                inBuffer[(int)SettlerStatisticTypes.Settler]++; 
                            else
                                inBuffer[(int)SettlerStatisticTypes.Worker]++; 
                        }
                        else
                            inBuffer[(int)SettlerStatisticTypes.Settler]++; 
                    }break;

                    default:
                        throw new NotImplementedException("Add counting for this type of movable!");
                }

                return true;
            });
        }
    }
}
